﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace Djs.HW.Util.MsCommPort
{
	/// <summary>
	/// Třída pro měření IR packetu na pozadí (v odděleném threadu)
	/// </summary>
	public class IrReceiver : IDisposable
	{
		#region KONSTRUKCE, PROMĚNNÉ, DISPOSE
		/// <summary>
		/// 
		/// </summary>
		/// <param name="outTime">Čas mezi dvěma pakety, milisekundy</param>
		public IrReceiver(decimal outTime)
		{
			this._OutTime = outTime / 1000M;
		}
		/// <summary>
		/// Uvolní data z paměti
		/// </summary>
		public void Dispose()
		{
			this._Stop();
		}
		/// <summary>
		/// Název portu
		/// </summary>
		private string _PortName;
		/// <summary>
		/// Požadavek na ukončení smyčky měření času
		/// </summary>
		private bool _CancelMeasure;
		/// <summary>
		/// Zdroj času
		/// </summary>
		private System.Diagnostics.Stopwatch _Timer;
		/// <summary>
		/// Seriový port
		/// </summary>
		private System.IO.Ports.SerialPort _Port;
		/// <summary>
		/// Thread, v němž běží čtení portu
		/// </summary>
		private System.Threading.Thread _Thread;
		/// <summary>
		/// Čas, po jehož uplynutí beze změny odešleme paket
		/// </summary>
		private decimal _OutTime;
		/// <summary>
		/// Event, který je volán po příjmu jednoho paketu s daty
		/// </summary>
		public event IrReceivedPacketEventHandler IrReceivedPacket;
		#endregion
		#region START A STOP MĚŘENÍ
		/// <summary>
		/// Zahájí příjem dat na IR portu
		/// </summary>
		/// <param name="portName"></param>
		public void StartReceive(string portName)
		{
			if (this.IrReceivedPacket == null)
				throw new InvalidOperationException("Chyba: nelze startovat IR čtení bez zadání eventu IrReceivedPacket!");

			this._Stop();

			this._PortName = portName;

			this._Thread = new System.Threading.Thread(new System.Threading.ThreadStart(this._ReadStart));
			this._Thread.IsBackground = true;
			this._Thread.Priority = System.Threading.ThreadPriority.AboveNormal;
			this._Thread.Name = "IrReceiver";
			this._CancelMeasure = false;
			this._Thread.Start();
		}
		public void StopReceive()
		{
			this._Stop();
		}
		private void _Stop()
		{
			this._CancelMeasure = true;
			this._WaitTime(10);

			if (this._Thread != null)
			{
				this._Thread.Abort();
				this._Thread = null;
			}
			if (this._Port != null)
			{
				if (this._Port.IsOpen)
					this._Port.Close();
				this._Port = null;
			}
		}
		#endregion
		#region FYZICKÉ ŘÍZENÍ PROCESŮ ČTENÍ DAT Z PORTU A VOLÁNÍ EVENTU
		private void _ReadStart()
		{
			this._OpenPort(this._PortName);
			this._StartIR();
			this._WaitTime(100);
			this._StartTimer();
			this._MeasureIR();
			this._StopIR();
			this._ClosePort();
		}
		/// <summary>
		/// Trvalá smyčka pro měření času paketů
		/// </summary>
		private void _MeasureIR()
		{
			bool last36 = this._Port.CDHolding;
			bool last38 = this._Port.DsrHolding;
			CarrierFrequency carrier = CarrierFrequency.None;
			decimal timeFq = (decimal)System.Diagnostics.Stopwatch.Frequency;
			ReceivedPacket packet = null;     // Musím inicializovat kvůli překladači
			int packetNo = 0;
			long lastTime = 0;
			decimal outTime = this._OutTime;  // Timeout: pokud máme otevřený paket, a takovouto dobu nedojde ke změně signálu, odešleme event s nastřádaným paketem
			long outLong = (long)(outTime * (decimal)timeFq);     // Timeout v počtu ticků
			while (!this._CancelMeasure)
			{
				long currTime = this._Timer.ElapsedTicks;
				bool curr36 = this._Port.CDHolding;
				bool curr38 = this._Port.DsrHolding;
				switch (carrier)
				{
					case CarrierFrequency.None:
						// Čekám jakoukoli změnu:
						if (last36 == curr36 && last38 == curr38)
							continue;     // Čekám jakoukoli změnu, a ono nic
						
						// Zahajuji načítání prvního pulsu (právě začal):
						if (last36 != curr36)
							// Došlo ke změně na lince 36:
							carrier = CarrierFrequency.F36KHz;
						else if (last38 != curr38)
							// Došlo ke změně na lince 38:
							carrier = CarrierFrequency.F38KHz;
						packet = new ReceivedPacket(++packetNo, carrier);
						lastTime = currTime;
						break;

					case CarrierFrequency.F36KHz:
						// Čekám změnu jen na lince 36. Linky 38 si nevšímám:
						if (last36 == curr36)
						{
							if ((currTime - lastTime) < outLong)    // Není timeout:
								continue;
							this.OnIrReceivedPacket(packet);
							carrier = CarrierFrequency.None;
							continue;
						}

						packet.PacketItems.Add(ReceivedItem.New(lastTime, currTime, timeFq, curr36));
						lastTime = currTime;
						last36 = curr36;
						break;

					case CarrierFrequency.F38KHz:
						// Čekám změnu jen na lince 36. Linky 38 si nevšímám:
						if (last38 == curr38)
						{
							if ((currTime - lastTime) < outLong)    // Není timeout:
								continue;
							this.OnIrReceivedPacket(packet);
							carrier = CarrierFrequency.None;
							continue;
						}

						packet.PacketItems.Add(ReceivedItem.New(lastTime, currTime, timeFq, curr38));
						last38 = curr38;
						lastTime = currTime;
						break;

				}
			}
		}

		private void OnIrReceivedPacket(ReceivedPacket receivedPacket)
		{
			if (this.IrReceivedPacket != null)
				this.IrReceivedPacket(this, new PacketReceivedEventArgs(receivedPacket));
		}
		/// <summary>
		/// Zapne IR čidlo
		/// </summary>
		private void _StartIR()
		{
			this._Port.RtsEnable = true;        // Zapnu napájení
		}
		/// <summary>
		/// Počká nějaký čas
		/// </summary>
		/// <param name="miliseconds"></param>
		private void _WaitTime(int miliseconds)
		{
			System.Threading.Thread.Sleep(miliseconds);
		}
		/// <summary>
		/// Vypne IR čidlo
		/// </summary>
		private void _StopIR()
		{
			this._Port.RtsEnable = false;       // Vypnu napájení
		}
		/// <summary>
		/// Otevře port, nic víc po tom nedělá
		/// </summary>
		/// <param name="portName"></param>
		private void _OpenPort(string portName)
		{
			this._ClosePort();
			try
			{
				this._Port = new System.IO.Ports.SerialPort(portName);
				this._Port.Open();
			}
			catch (Exception e)
			{ }

		}
		/// <summary>
		/// Zapne časovač
		/// </summary>
		private void _StartTimer()
		{
			this._Timer = new System.Diagnostics.Stopwatch();
			this._Timer.Reset();
			this._Timer.Start();
		}
		/// <summary>
		/// Zavře port, pokud je otevřený.
		/// </summary>
		private void _ClosePort()
		{
			if (this._Port == null) return;

			if (this._Port.IsOpen)
				this._Port.Close();

			this._Port = null;
		}
		#endregion
		#region CLASS ReceivedItem, ReceivedPacket, PacketReceivedEventArgs; DELEGATE IrReceivedPacketEventHandler
		/// <summary>
		/// Třída pro jednu položku přijatých dat.
		/// Popisuje dobu, po kterou je vstupní signál na dané úrovni.
		/// </summary>
		public class ReceivedItem
		{
			public ReceivedItem(decimal time, bool value)
			{
				this.Time = time;
				this.Value = value;
			}
			public override string ToString()
			{
				decimal milisec = this.Time * 1000M;
				return "{" + (this.Value ? "Hi" : "Lo") + ": Time=" + milisec.ToString("# ##0.000") + "}";
			}
			/// <summary>
			/// Doba, po kterou je signál na úrovni Value
			/// </summary>
			public decimal Time { get; protected set; }
			/// <summary>
			/// Úroveň signálu
			/// </summary>
			public bool Value { get; protected set; }
			/// <summary>
			/// Vrátí klon sebe sama
			/// </summary>
			/// <returns></returns>
			public ReceivedItem Clone()
			{
				ReceivedItem clone = new ReceivedItem(this.Time, this.Value);
				return clone;
			}
			/// <summary>
			/// Statický konstruktor
			/// </summary>
			/// <param name="lastTime"></param>
			/// <param name="currTime"></param>
			/// <param name="timeFq"></param>
			/// <param name="value"></param>
			/// <returns></returns>
			internal static ReceivedItem New(long lastTime, long currTime, decimal timeFq, bool value)
			{
				decimal time = (decimal)(currTime - lastTime) / timeFq;
				return new ReceivedItem(time, value);
			}
		}
		/// <summary>
		/// Sada vstupních hodnot
		/// </summary>
		public class ReceivedPacket
		{
			public ReceivedPacket(int packetNo, CarrierFrequency frequencyCarrier)
			{
				this.PacketNo = packetNo;
				this.FrequencyCarrier = frequencyCarrier;
				this.PacketItems = new List<ReceivedItem>();
			}
			/// <summary>
			/// Frekvence nosné vlny
			/// </summary>
			public int PacketNo { get; protected set; }
			/// <summary>
			/// Frekvence nosné vlny
			/// </summary>
			public CarrierFrequency FrequencyCarrier { get; protected set; }
			/// <summary>
			/// Časové položky
			/// </summary>
			public List<ReceivedItem> PacketItems { get; protected set; }
			/// <summary>
			/// Sumární čas Time ze všech PacketItems
			/// </summary>
			public decimal TimeTotal
			{
				get
				{
					decimal timeTotal = 0M;
					foreach (ReceivedItem item in this.PacketItems)
						timeTotal += item.Time;
					return timeTotal;
				}
			}
			/// <summary>
			/// Vrátí klon sebe sama
			/// </summary>
			/// <returns></returns>
			public ReceivedPacket Clone()
			{
				ReceivedPacket clone = new ReceivedPacket(this.PacketNo, this.FrequencyCarrier);
				foreach (ReceivedItem item in this.PacketItems)
					clone.PacketItems.Add(item.Clone());
				return clone;
			}
			/// <summary>
			/// Metoda vrátí výsledná naměřená data
			/// </summary>
			/// <returns></returns>
			internal string GetTimeResult()
			{
				StringBuilder sb = new StringBuilder();
				sb.AppendLine("Input sequence Nr.: " + this.PacketNo.ToString());
				sb.AppendLine("  Carrier frequency: " + this.FrequencyCarrier.ToString());

				List<InputRange> keyTimes = this.GetKeyTimes(0.18M, 0.00005M);
				sb.AppendLine("  Key Times:" + this.FrequencyCarrier.ToString());
				foreach (InputRange range in keyTimes)
					sb.AppendLine(range.ToString());

				foreach (ReceivedItem item in this.PacketItems)
				{
					decimal time = item.Time * 1000M;
					sb.AppendLine((item.Value ? "H" : "L") + " : " + time.ToString("##0.##"));
				}
				return sb.ToString();
			}
			/// <summary>
			/// Metoda vrátí soupis klíčových časů
			/// </summary>
			/// <returns></returns>
			internal List<InputRange> GetKeyTimes(decimal tolerationA, decimal tolerationB)
			{
				List<ReceivedItem> items = this.PacketItems;

				// Sestavím pole rozsahů prokaždý vstupní čas:
				List<InputRange> ranges = new List<InputRange>();
				for (int i = 0; i < items.Count; i++)
				{
					ReceivedItem itemInp = items[i];
					InputRange range = new InputRange(itemInp.Time, i, tolerationA, tolerationB);

					for (int n = 0; n < items.Count; n++)
					{
						if (n == i) continue;
						ReceivedItem itemOth = items[n];
						range.TimeAddIfInToleration(itemOth.Time, n);
					}

					ranges.Add(range);
				}

				// Seřadím pole rozsahů podle počtu prvků, které pokrývají:
				ranges.Sort(InputRange.CompareSortCount);

				// Projdu setříděné pole, a do výstupu zařadím průměrný čas, a zapamatuji si všechny pokryté vstupní prvky (abych na nějaká nezapomněl):
				List<InputRange> times = new List<InputRange>();
				Hashtable ht = new Hashtable();
				foreach (InputRange range in ranges)
				{
					if (!ht.ContainsKey(range.IndexCurr))
					{
						times.Add(range);
						ht.Add(range.IndexCurr, null);

						foreach (int indexOth in range.IndexList)
						{
							if (!ht.ContainsKey(indexOth))
								ht.Add(indexOth, null);
						}
					}
				}
				times.Sort(InputRange.CompareSortTimeMin);

				// Pokud najdu za sebou dva intervaly, které se překrývají, spojím je:
				List<InputRange> result = new List<InputRange>();
				foreach (InputRange range in times)
				{
					int last = result.Count - 1;
					if (last == -1 || range.TimeMin > result[last].TimeMax)
						result.Add(range);
					else
						result[last].JoinWith(range);
				}
				result.Sort(InputRange.CompareSortCount);

				return result;
			}
		}
		/// <summary>
		/// Třída pro event po příjmu jednoho paketu
		/// </summary>
		public class PacketReceivedEventArgs : EventArgs
		{
			public PacketReceivedEventArgs(ReceivedPacket receivedPacket)
				: base()
			{
				this.ReceivedPacket = receivedPacket;
			}
			/// <summary>
			/// Sada vstupních hodnot přijatého paketu
			/// </summary>
			public ReceivedPacket ReceivedPacket { get; protected set; }
		}
		/// <summary>
		/// Předpis pro handler eventu 
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		public delegate void IrReceivedPacketEventHandler (object sender, PacketReceivedEventArgs e);
		/// <summary>
		/// Vstupní kanál = nosná frekvence
		/// </summary>
		public enum CarrierFrequency
		{
			None,
			F36KHz,
			F38KHz
		}
		#endregion
		#region
		internal class InputRange
		{
			internal InputRange(decimal timeCurr, int index, decimal tolerationA, decimal tolerationB)
			{
				this.IndexList = new List<int>();
				this.TimeCurr = timeCurr;
				this.TimeMin = timeCurr;
				this.TimeMax = timeCurr;
				this._TimeRangeMin = timeCurr * (1M - tolerationA) - tolerationB;
				this._TimeRangeMax = timeCurr * (1M + tolerationA) + tolerationB;
				this.IndexCurr = index;
				this.TimeAdd(timeCurr, -1);
			}
			public override string ToString()
			{
				decimal timeMilisec = this.TimeAverage * 1000M;
				decimal timeMin = this.TimeMin * 1000M;
				decimal timeMax = this.TimeMax * 1000M;
				string fmt = "##0.00";
				return "Average Time=" + timeMilisec.ToString(fmt) + "ms; Count=" + this.TimeCount.ToString() + "; Range={" + timeMin.ToString(fmt) + " ÷ " + timeMax.ToString(fmt) + "}";
			}
			/// <summary>
			/// Vrátí true, pokud je dané číslo v toleranci tohoto range
			/// </summary>
			/// <param name="time"></param>
			/// <returns></returns>
			internal bool IsInToleration(decimal time)
			{
				return (time >= this._TimeRangeMin && time <= this._TimeRangeMax);
			}
			/// <summary>
			/// Přidá další hodnotu (čas) do tohoto range, pokud je v dané toleranci
			/// </summary>
			/// <param name="time"></param>
			internal void TimeAddIfInToleration(decimal time, int index)
			{
				if (this.IsInToleration(time))
					this.TimeAdd(time, index);
			}
			/// <summary>
			/// Přidá další hodnotu (čas) do tohoto range
			/// </summary>
			/// <param name="time"></param>
			internal void TimeAdd(decimal time, int index)
			{
				this.TimeCount++;
				this.TimeSum += time;
				if (time < this.TimeMin)
					this.TimeMin = time;
				if (time > this.TimeMax)
					this.TimeMax = time;
				if (index >= 0)
					this.IndexList.Add(index);
			}
			internal decimal TimeCurr;
			private decimal _TimeRangeMin;
			private decimal _TimeRangeMax;
			internal decimal TimeMin;
			internal decimal TimeMax;
			internal int TimeCount;
			internal decimal TimeSum;
			internal int IndexCurr;
			internal List<int> IndexList;
			/// <summary>
			/// Průměrný čas
			/// </summary>
			internal decimal TimeAverage
			{
				get { return this.TimeSum / this.TimeCount; }
			}
			/// <summary>
			/// Setřídí podle počtu prvků sestupně a podle indexu currnt prvku vzestupně
			/// </summary>
			/// <param name="a"></param>
			/// <param name="b"></param>
			/// <returns></returns>
			internal static int CompareSortIndex(InputRange a, InputRange b)
			{
				int cmp = a.IndexCurr.CompareTo(b.IndexCurr);
				return cmp;
			}
			/// <summary>
			/// Setřídí podle počtu prvků sestupně a podle indexu currnt prvku vzestupně
			/// </summary>
			/// <param name="a"></param>
			/// <param name="b"></param>
			/// <returns></returns>
			internal static int CompareSortCount(InputRange a, InputRange b)
			{
				int cmp = b.IndexList.Count.CompareTo(a.IndexList.Count);
				if (cmp != 0) return cmp;
				cmp = a.IndexCurr.CompareTo(b.IndexCurr);
				return cmp;
			}
			/// <summary>
			/// Setřídí podle dolní hodnoty TimeMin ASC
			/// </summary>
			/// <param name="a"></param>
			/// <param name="b"></param>
			/// <returns></returns>
			internal static int CompareSortTimeMin(InputRange a, InputRange b)
			{
				int cmp = a.TimeMin.CompareTo(b.TimeMin);
				return cmp;
			}

			internal void JoinWith(InputRange range)
			{
				this.TimeMin = (range.TimeMin < this.TimeMin ? range.TimeMin : this.TimeMin);
				this.TimeMax = (range.TimeMax > this.TimeMax ? range.TimeMax : this.TimeMax);
				this.TimeCount += range.TimeCount;
				this.TimeSum += range.TimeSum;
				this.IndexList.AddRange(range.IndexList);
			}
		}
		#endregion
	}
}
